﻿
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using VerifyArgs;

namespace DZParallelLib.Utility
{
    /// <summary>
    /// A class to help with caching images that have a lot of I/O associated with them.
    /// </summary>
    class ImageLevelCache
    {
        /// <summary>
        /// Create the cache.
        /// </summary>
        /// <param name="maxID">The maximum ID of the image in this collection</param>
        /// <param name="nLevels">Number of levels in the image hierarchy.</param>
        public ImageLevelCache(int maxID, int nLevels, int tileSize, DirectoryInfo outputDirectory)
        {
            TilesPerLevelToCache = 16;
            _tileSize = tileSize;
            _outputDir = outputDirectory;
            CalculateLevelTable(maxID, nLevels);
        }

        /// <summary>
        /// Calculate how many tiles there are in each level. Only called during initalization
        /// </summary>
        private void CalculateLevelTable(int maxID, int nLevels)
        {
            _tilesPerLevel = new int[nLevels];
            double imagesForThisLevel = maxID;
            for (int i = nLevels - 1; i >= 0; i--)
            {
                _tilesPerLevel[i] = (int)Math.Ceiling(imagesForThisLevel);
                if (_tilesPerLevel[i] == 0)
                    _tilesPerLevel[i] = 1;
                imagesForThisLevel = imagesForThisLevel / 4.0;
            }
        }

        /// <summary>
        /// Get/Set the number of tiles per level below which we will cache the tiles.
        /// </summary>
        /// <remarks>
        /// If a level has less than this number of tiles in it, then we will hold the images in memory. Otherwise, the
        /// file will be written out each time.
        /// </remarks>
        public int TilesPerLevelToCache { get; set; }

        /// <summary>
        /// Now many tiles there will be in each level.
        /// </summary>
        private int[] _tilesPerLevel = null;

        /// <summary>
        /// The tile size for the images.
        /// </summary>
        private readonly int _tileSize;

        /// <summary>
        /// Base directory for the output images.
        /// </summary>
        private readonly DirectoryInfo _outputDir;

        /// <summary>
        /// Holds onto the image collection that we are caching.
        /// </summary>
        Dictionary<Tuple<int, ulong, ulong>, Tuple<Graphics, Image>> _imageCache = new Dictionary<Tuple<int, ulong, ulong>, Tuple<Graphics, Image>>();

        /// <summary>
        /// Keep track of what images we are writing, etc.
        /// </summary>
        private CacheFileLocker _locker = new CacheFileLocker();

        /// <summary>
        /// Called to clean up any held graphics items. Make sure to dispose of all the Graphics objects!
        /// This method should be in a finally handler if you want to make sure there are no leaking resources!
        /// </summary>
        internal async Task CleanUp()
        {
            try
            {
                var saveItAll = _imageCache.Keys.Select(cacheIndex =>
                {
                    var outputFile = TileFilename(cacheIndex.Item1, cacheIndex.Item2, cacheIndex.Item3);
                    return SaveImageToDisk(outputFile, _imageCache[cacheIndex].Item2);
                });
                await Task.WhenAll(saveItAll.ToArray());
            }
            finally
            {
                foreach (var item in _imageCache.Values)
                {
                    item.Item1.Dispose();
                }
                _imageCache.Clear();
            }
        }

        /// <summary>
        /// Write out a particular image.
        /// </summary>
        /// <returns></returns>
        public async Task UpdateImage(
            int level,
            Tuple<ulong, ulong> tileIndex,
            Action<Graphics> saveTheData)
        {
            Verify.Args(new { level }).InRange(0, _tilesPerLevel.Length - 1);
            Verify.Args(new { tileIndex, saveTheData }).NotNull();

            // If this is already in the cache, then we just do what we'd normally do.

            var cacheIndex = Tuple.Create(level, tileIndex.Item1, tileIndex.Item2);
            var lockHandle = await _locker.Lock(cacheIndex);
            try
            {
                Tuple<Graphics, Image> gm;
                if (_imageCache.TryGetValue(cacheIndex, out gm))
                {
                    saveTheData(gm.Item1);
                    return;
                }

                // If we've dropped through to here, then we don't have this guy in the cache. So we are
                // going ot have to load it from a file.

                var tileFFInfo = TileFilename(level, tileIndex.Item1, tileIndex.Item2);

                Graphics g = null;
                Bitmap bm = null;
                try
                {
                    // If the file exists load it, otherwise create a blank graphics to draw on.
                    if (tileFFInfo.Exists)
                    {
                        // We don't use FromFile becuase it seems to lock
                        // the file on the disk.

                        await tileFFInfo.LoadImage(i => bm = new Bitmap(i));
                        g = Graphics.FromImage(bm);
                    }
                    else
                    {
                        bm = new Bitmap(_tileSize, _tileSize);
                        g = Graphics.FromImage(bm);
                        g.FillRectangle(Brushes.Black, 0, 0, _tileSize, _tileSize);
                    }

                    // We have a valid graphics! Add the image
                    saveTheData(g);

                    // If the image is to be cached, add it to the cache. Otherwise, we should write it out to a file.
                    if (_tilesPerLevel[level] <= TilesPerLevelToCache)
                    {
                        _imageCache[cacheIndex] = Tuple.Create(g, bm as Image);
                        g = null;
                        bm = null;
                    }
                    else
                    {
                        await SaveImageToDisk(tileFFInfo, bm);
                    }

                }
                finally
                {
                    if (g != null)
                        g.Dispose();
                    if (bm != null)
                        bm.Dispose();
                }
            }
            finally
            {
                lockHandle.Dispose();
            }
        }

        /// <summary>
        /// Save an image to disk
        /// </summary>
        /// <param name="tileFFInfo"></param>
        /// <param name="img"></param>
        private static async Task SaveImageToDisk(FileInfo tileFFInfo, Image img)
        {
            if (!tileFFInfo.Directory.Exists)
                tileFFInfo.Directory.Create();

            using (var mr = new MemoryStream())
            {
                img.Save(mr, ImageFormat.Png);
                mr.Seek(0, SeekOrigin.Begin);
                using (var writer = tileFFInfo.OpenWrite())
                {
                    await mr.CopyToAsync(writer);
                }
            }
        }

        /// <summary>
        /// Return the filename for a tile at a particular level
        /// </summary>
        /// <param name="level"></param>
        /// <param name="tileIndex"></param>
        /// <returns></returns>
        private FileInfo TileFilename(int level, ulong xloc, ulong yloc)
        {
            return new FileInfo(Path.Combine(_outputDir.FullName, level.ToString(), string.Format("{0}_{1}.png", xloc, yloc)));
        }

    }
}
